忍者讀書會-老手看函式:理解函式呼叫

2022-05-08 Sun

本篇參與忍者:JavaScript 開發技巧探秘 第二版讀書會的導讀內容加上自己所蒐尋的資料後所構成的文章。

Javascript 的函式特性

Javascript 不支援函式重載(funciton overloading)及可變函式(variadic funciton)

function overloading 以 java 為例 > variadic funciton 以 java 為例

函式隱含的參數

隱含的意思是沒有明確列在函式署名(function signature),換言之這些參數將會默默傳遞給函式或者說在函式宣告的時候將會預設附帶在函式裡面。

  • this 被稱為函式的背景空間(context)
  • arguments 代表在進行函式呼叫時所傳遞的所有引數

arguments 參數

印出 arguments

以下面的範例可以印出 arguments 來得知內容物為何

function argumentsExample(a, b, c){
  console.log(arguments)
}
argumentsExample(1,2,3,4,5);

印出結果如下圖

arguments 非陣列

arguments 是一個類陣列(array-like),但不是一個真正的陣列,如果嘗試著使用 array method 的話,將會無法使用。

function argumentsExample(a, b, c){
    arguments.forEach(el=>(console.log(el)));
}
argumentsExample(1,2,3,4,5);
//這個範例會出錯

如下圖

若想要遍歷傳進來的參數的話,請改用 forLoop 或者改使用不定參數(又稱其餘參數 rest parameter)的話就能使用 array method 了

arguments 物件作為函式參數的別名

這邊的意思是,當我們改動 arguments 的時候,參數也會被改動 如下面的範例當我們對 arguments[0]改動的時候,這時候印出 a 參數的話也會從原先應當印出 1 的情形,變成印出 ninja。

function argumentsExample(a, b, c){
    arguments[0] = "ninja";
    console.log(a);
}
argumentsExample(1,2,3,4,5);

使用嚴格模式避免別名

若要避免改動 arguments 物件的話,可以使用嚴格模式(strict mode)

嚴格模式可以禁止一些 javascript 不安全的功能或是讓一些原本 javascript 的行為有所轉變。

在原先的程式碼當中加入"use strict",此時 a 將不會被改變

"use strict"
function argumentsExample(a, b, c){
    arguments[0] = "ninja";
    console.log(a);
}
argumentsExample(1,2,3,4,5);

this 介紹函式背景空間(context)

物件導向語言,例如 java,this 通常只向該方法的類別實例 this 參數指向的東西不僅由函式的定義方式和位置做決定(例如 java 和 C#),也會受到函式的呼叫方式影響。

this 的嚴格模式

普通模式

function ninja(){
 return this;
}
ninja();
//這邊的this指向的是winodows

嚴格模式

function samurai (){
    "use strict"
    return this;
}
samurai()
//這邊this指向的是undefined

函式的四種呼叫方式

  • skulk() 使用一般小括弧的方式
  • 作為一個方法(method) 例如 ninja.skulk() 類似物件導向的程式設計方式
  • 建構式函式 new Ninja()
  • 藉由 apply 和 call 的方式 skulk.call(ninja) 或 skulk.apply(ninja)

視為函式進行呼叫

當我們不透過任何物件,而是直接使用小括號()運算子來進行呼叫,參見以下的範例

function ninja() {
  return this;//將會回傳windows
}

function samurai() {
  "use strict";
  return this;//在嚴格模式下會回傳undefined
}

作為方法呼叫

在作為某物件屬性的值時,該函式的 this 藉由該物件呼叫的時候會回傳該物件,以下的值都是共用whatsMyContext這個函式,但由於根據不同的物件呼叫,this 指向的內容也不一樣。

function whatsMyContext() {
  return this;
}
var getMyThis = whatsMyContext;

var ninja1 = {
  getMyThis: whatsMyContext //ninja1.getMyThis() 回傳ninja1這個物件
};
var ninja2 = {
  getMyThis: whatsMyContext //ninja2.getMyThis()回傳ninja這個物件
};

作為建構器呼叫

函式建構器與建構器函式並非一樣,函式建構器指的是Function('a','b','return a + b'),使用我透過字串的方式建立一個函式,換句話說我們可以動態建立字串(也就是使用變數的方式而且值是字串)來建立函式,而建構器函式的功用是建立和初始化物件實例的函式 參見以下範例

 function Ninja() {
  this.skulk = function() {
    return this;
  };
}
var ninja1 = new Ninja();
/*
上面的程式碼會發生幾件事情,首先建立一個空物件、將skulk作為屬性加入到該物件當中,值為function(){return this},這時候的this始終指向ninja1
*/
var ninja2 = new Ninja();

使用關鍵字 new 呼叫函式會進行下動作

  1. 建立一個新的空物件
  2. 此物件被當成 this 參數傳遞給建構器成為該建構器的函式背景空間(他的 this)
  3. new 運算子會回傳新建立的物件(也有例外)

建構器函式若回傳基礎型值(return primitive value)

如果在建構器函式的 function return 基礎型別的話,即便使用 new 關鍵字建立新物件時,將不會影響其建立新物件。 另外如果沒有使用 new 關鍵字,而是直接執行 Ninja 則會得到剛剛的 retrun 值 1 參見以下範例

function Ninja() {
  this.skulk = function () {
    return true;
  };
  return 1;
}
Ninja()===1; //true
var ninja = new Ninja();//該函式使用new關鍵字,則此函式依然是作為建構器函式使用

建構器函式回傳物件(return 非基礎型別)

var puppet = {
  rules: false
};

function Emperor() {
  this.rules = true;
  return puppet;
}
var emperor = new Emperor();

在 Emperor 建構器函式當中,回傳的是一個全域物件,因此當我們使用 new Emperor 的時候就無法如期以 Emperor 做為建構器函式

建構器函式總結

  • 回傳一個物件的話,那麼使用 new 運算子的回傳值就會是該回傳物件
  • 回傳一個 primivite 型別的話,那麼使用 new 運算子該回傳值就會被忽略,一樣透過建構器
  • 建構器函式通常以名詞和大寫開頭作為命名(普通函式或方法通常則是動詞命名)

使用 apply、call 呼叫-綁定背景空間(this)

call 和 apply 可以稱為函式的方法 以下的範例中第 2 行的 this 和第 4 行的 this 不同,由於瀏覽器的事件處理會將函式呼叫的 this 定義為事件的目標元素

function Button(){
  this.clicked = false;
  this.click = function() {
    this.clicked = true;
    assert(button.clicked,"The button has been clicked");
  }
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);
//瀏覽器的事件處理會將函式呼叫的this定義為事件的目標元素

bind 範例

如果上面的範例想要明確正確指定 this 的話,就要使用 bind 如下

elem.addEventListener("click", button.click.bind(button));

bind 是會綁定給定的背景空間(context)並且創造出新的函式 如果將 butoon.click 和 button.click.bind(button)做三個等於的話則會顯示 false

換成 call 的話會如何?

如果換成 call 的話會被呼叫,由於這裡帶入的是 callbackFunction,只需要帶入函式就好,而不需要被呼叫,因此不適用。

elem.addEventListener("click", button.click.call(button));
//這樣寫按鈕沒按就會被執行

藉由 apply 和 call 提供函式的背景空間

function juggle() {
  var result = 0;
  for (var n = 0; n < arguments.length; n++) {
    result += arguments[n];
  }
  this.result = result;
}

var ninja1 = {};
var ninja2 = {};
//console.log(ninja1)會發現他多了一個result的值
juggle.apply(ninja1,[1,2,3,4]);
juggle.call(ninja2, 5,6,7,8);

簡略版的 forEach

透過 call 和 apply 的特性可以嘗試著做出簡略版的 forEach 範例如下

function forEach(list, callback) {
  for (var n = 0; n < list.length; n++) {
    callback.call(list[n], n);
  }
}
var weapons = [{ type:'shuriken'},
               { type:'katana'},
               { type:'nunchucks'}];
forEach(weapons,function(index){
  //你要做的事情
})

apply、call、bind 總結

  • call、apply 作為呼叫函式的一種方式
  • bind 將其背景空間包裹後回傳一個函式
  • call、apply 的第一個參數會做為目標函式的背景空間

箭頭函式

先前的範例可以透過箭頭函式改寫

  • 箭頭函式沒有自己的 this
  • 透過宣告時候的 this 做為函式 this(背景空間或稱 context)
function Button() {
  this.clicked = false;
  this.click = () => {
    this.clicked = true //這時候的this取決於this.click而this.click指向的是被建立出來的物件
    assert(button.clicked, "The button has been clicked");
  }
}
var button = new Button();
var elem = document.getElementById("test");
elem.addEventListener("click", button.click);

箭頭函式的 this 是宣告的當下被決定

由於箭頭函式沒有自己的 this 因此下面的範例的 this 是宣告的當下決定

var button = {
  clicked: false,
  click: () => {
    this.clicked = true; //被宣告的當下 this指向的是全域
  }
}